/*
 *    Geotoolkit - An Open Source Java GIS Toolkit
 *    http://www.geotoolkit.org
 *
 *    (C) 2010, Geomatys
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */

package org.geotoolkit.xml;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.Result;
import javax.xml.transform.stax.StAXResult;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.xml.Namespaces;


/**
 * An abstract class for all stax stream writer.<br/>
 * Writers for a given specification should extend this class and
 * provide appropriate write methods.<br/>
 * <br/>
 * Example : <br/>
 * <pre>
 * {@code
 * public class UserWriter extends StaxStreamWriter{
 *
 *   public void write(User user) throws XMLStreamException{
 *      //casual stax writing operations
 *      writer.writeStartElement(...
 *   }
 * }
 * }
 * </pre>
 * And should be used like :<br/>
 * <pre>
 * {@code
 * final UserWriter instance = new UserWriter();
 * try{
 *     instance.setOutput(stream);
 *     instance.write(aUser);
 * }finally{
 *     instance.dispose();
 * }
 * </pre>
 *
 * @author Johann Sorel (Geomatys)
 * @module
 */
public abstract class StaxStreamWriter extends AbstractConfigurable {

    /**
     * Logger for this writer.
     */
    protected static final Logger LOGGER = Logging.getLogger("org.geotoolkit.xml");

    /**
     * The factory built-in the JDK. This is different than {@link XMLOutputFactory#newFactory()},
     * which may be the Jackson factory. We want the standard factory when creating an {@link XMLEventWriter}
     * writing to {@link XMLStreamWriter} because Jackson seems to not support this configuration.
     */
    private static final XMLOutputFactory STANDARD_FACTORY;
    static {
        try {
            // TODO: try using XMLOutputFactory.newDefaultFactory() with JDK9 instead.
            STANDARD_FACTORY = (XMLOutputFactory) Class.forName("com.sun.xml.internal.stream.XMLOutputFactoryImpl").newInstance();
        } catch (ReflectiveOperationException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    protected XMLStreamWriter writer;

    /**
     * Store the output stream if it was generated by the parser itself.
     * It will closed on the dispose method or when a new input is set.
     */
    private OutputStream targetStream;

    private int lastUnknowPrefix = 0;

    private final Map<String,String> unknowNamespaces = new HashMap<>();

    protected StaxStreamWriter(){
    }

    /**
     * Acces the underlying stax writer.
     * This method is used when several writer are wrapping a single writer.
     * Like when an Symbology Encoding writer wraps a Filter writer.
     * <br>
     * It can also be used to write tag before or after this writer is used.
     */
    public XMLStreamWriter getWriter(){
        return writer;
    }

    /**
     * close potentiel previous stream and cache if there are some.
     * This way the writer can be reused for a different output later.
     * The underlying stax writer will be closed.
     */
    public void reset() throws IOException, XMLStreamException {
        if (writer != null) {
            writer.close();
            writer = null;
        }
        if (targetStream != null) {
            targetStream.close();
            targetStream = null;
        }
    }

    /**
     * Release potentiel locks or opened stream.
     * Must be called when the writer is not needed anymore.
     * It should not be used after this method has been called.
     */
    public void dispose() throws IOException, XMLStreamException {
        reset();
    }

    /**
     * Set the output for this writer.<br/>
     * Handle types are :<br/>
     * - java.io.File<br/>
     * - java.io.Writer<br/>
     * - java.io.OutputStream<br/>
     * - javax.xml.stream.XMLStreamWriter<br/>
     * - javax.xml.transform.Result<br/>
     */
    public void setOutput(Object output) throws IOException, XMLStreamException {
        reset();

        if (output instanceof XMLStreamWriter) {
            writer = (XMLStreamWriter) output;
            return;
        }

        if (output instanceof File) {
            targetStream = new FileOutputStream((File)output);
            final BufferedOutputStream bout = new BufferedOutputStream(targetStream);
            output = bout;
        }
        if (output instanceof Path) {
            targetStream = Files.newOutputStream((Path) output, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
            final BufferedOutputStream bout = new BufferedOutputStream(targetStream);
            output = bout;
        }


        writer = toWriter(output);
    }

    /**
     * Write a new tag with the text corresponding to the given value.
     * The tag won't be written if the value is null.
     *
     * @param namespace  namespace of the wanted tag
     * @param localName  local name of the wanted tag
     * @param value      text value to write
     */
    protected void writeSimpleTag(final String namespace, final String localName, final Object value) throws XMLStreamException {
        if (value != null) {
            writer.writeStartElement(namespace, localName);
            writer.writeCharacters(value.toString());
            writer.writeEndElement();
        }
    }

    /**
     * Marshals the given object to the given marshaller, with a workaround for Jackson unsupported operation.
     * This workaround avoid the following exception:
     *
     * <blockquote><pre>java.lang.IllegalArgumentException: Can not instantiate a writer for XML result type class javax.xml.transform.stax.StAXResult (unrecognized type)
     *     at com.ctc.wstx.stax.WstxOutputFactory.createSW(WstxOutputFactory.java:348)
     *     at com.ctc.wstx.stax.WstxOutputFactory.createXMLEventWriter(WstxOutputFactory.java:110)
     *     at org.apache.sis.xml.OutputFactory.createXMLEventWriter(OutputFactory.java:140)
     *     at org.apache.sis.xml.PooledMarshaller.marshal(PooledMarshaller.java:317)</pre></blockquote>
     */
    protected void marshal(final Marshaller marshaller, final Object element) throws JAXBException {
        if (!marshaller.getClass().getName().startsWith("org.apache.sis.xml.")) {
            marshaller.marshal(element, writer);
        } else try {
            /*
             * Apache SIS needs an XMLEventWriter, not a XMLStreamWriter. SIS normally wraps the stream
             * writer automatically, but this operation fails when Jackson is on the classpath (because
             * Jackson substitutes itself to the standard factory and does not support this operation).
             * So we have to explicitly use the standard factory.
             */
            marshaller.marshal(element, STANDARD_FACTORY.createXMLEventWriter(new StAXResult(writer)));
            /*
             * Do not close the XMLStreamWriter because user may continue writing to it.
             * Do not flush neither; the default XMLStreamWriterImpl does nothing more
             * than forwarding to java.io.Writer.flush() and flushing an output stream
             * have a performance impact. If the user really wants to flush, (s)he can
             * invoke XMLStreamWriter.flush() himself.
             */
        } catch (XMLStreamException e) {
            throw new JAXBException(e);
        }
    }

    /**
     * Creates a new XMLStreamWriter.
     *
     * @throws XMLStreamException if the output is not handled
     */
    private static XMLStreamWriter toWriter(final Object output) throws XMLStreamException {
        final XMLOutputFactory XMLfactory = XMLOutputFactory.newInstance();
        XMLfactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, Boolean.TRUE);

        if (output instanceof OutputStream) {
            return XMLfactory.createXMLStreamWriter((OutputStream) output, "UTF-8");
        } else if(output instanceof Result) {
            return XMLfactory.createXMLStreamWriter((Result) output);
        } else if(output instanceof Writer) {
            return XMLfactory.createXMLStreamWriter((Writer) output);
        } else {
            throw new XMLStreamException("Output type is not supported : "+ output);
        }
    }

    /**
     * Returns the prefix for the given namespace.
     *
     * @param namespace The namespace for which we want the prefix.
     */
    protected Prefix getPrefix(final String namespace) {
        String prefix = Namespaces.getPreferredPrefix(namespace, null);
        /*
         * temporary hack todo remove
         */
        if ("http://www.opengis.net/gml/3.2".equals(namespace)) {
            return new Prefix(false, "gml");
        }
        boolean unknow = false;
        if (prefix == null) {
            prefix = unknowNamespaces.get(namespace);
            if (prefix == null) {
                prefix = "ns" + lastUnknowPrefix;
                lastUnknowPrefix++;
                unknow = true;
                unknowNamespaces.put(namespace, prefix);
            }
        }
        return new Prefix(unknow, prefix);
    }

    /**
     * Inner class for handling prefix and if it is already known.
     */
    protected final class Prefix {
        public boolean unknow;
        public String prefix;

        public Prefix(final boolean unknow, final String prefix) {
            this.prefix = prefix;
            this.unknow = unknow;
        }
    }
}
